package org.xbl.xchain.sdk.crypto.algo;

import org.bitcoinj.core.ECKey;
import org.bitcoinj.core.SignatureDecodeException;
import org.bitcoinj.core.Utils;
import org.bouncycastle.asn1.sec.SECNamedCurves;
import org.bouncycastle.asn1.x9.X9ECParameters;
import org.bouncycastle.crypto.params.AsymmetricKeyParameter;
import org.bouncycastle.crypto.signers.ECDSASigner;
import org.xbl.xchain.sdk.crypto.hash.Ripemd;

import java.math.BigInteger;
import java.security.PrivateKey;
import java.security.PublicKey;

public class Secp256k1 extends ECAlgorithm {
    private Secp256k1() {
        super(AlgorithmType.SECP256K1);
    }

    public static Secp256k1 getInstance() {
        return Secp256k1.InnerSingleton.INSTANCE;
    }

    private static class InnerSingleton {
        private static final Secp256k1 INSTANCE = new Secp256k1();
    }

    @Override
    public byte[] sign(PrivateKey privateKey, byte[] msg) throws Exception {
        return sign(privateKey, msg, "SHA256withECDSA");
    }

    @Override
    public byte[] sign(AsymmetricKeyParameter privateKey, byte[] msg) throws Exception {
        ECDSASigner ecdsaSigner = new ECDSASigner();
        ecdsaSigner.init(true, privateKey);
        BigInteger[] sigs = ecdsaSigner.generateSignature(hashDigest(msg, 0, msg.length, "SHA-256"));
        BigInteger r = sigs[0];
        BigInteger s = sigs[1];
        X9ECParameters params = SECNamedCurves.getByName("secp256k1");
        // BIP62: "S must be less than or equal to half of the Group Order N"
        BigInteger overTwo = params.getN().shiftRight(1);
        if (s.compareTo(overTwo) == 1) {
            s = params.getN().subtract(s);
        }
        byte[] result = new byte[64];
        System.arraycopy(Utils.bigIntegerToBytes(r, 32), 0, result, 0, 32);
        System.arraycopy(Utils.bigIntegerToBytes(s, 32), 0, result, 32, 32);
        return result;
    }

    @Override
    public String genAddressFromPublicKey(String mainPrefix, PublicKey publicKey) throws Exception {
        byte[] hashBytes;
        byte[] bytes = parsePubKey(publicKey);

        hashBytes = hashDigest(bytes, 0, bytes.length, "SHA-256");
        byte[] md = Ripemd.ripemd160(hashBytes);
        return org.xbl.xchain.sdk.crypto.encode.Bech32.encode(mainPrefix, encode(0, md));
    }

    @Override
    public boolean verifySignature(PublicKey publicKey, byte[] msg, byte[] signature) throws Exception {
        return verifySignature(publicKey, msg, signature, "SHA256withECDSA");
    }

    @Override
    public boolean verifySignature(AsymmetricKeyParameter publicKey, byte[] msg, byte[] signature) throws Exception {
        byte[] buf = new byte[32];
        System.arraycopy(signature, 0, buf, 0, 32);
        BigInteger r = new BigInteger(1, buf);
        System.arraycopy(signature, 32, buf, 0, 32);
        BigInteger s = new BigInteger(1, buf);
        byte[] hashBytes= hashDigest(msg, 0, msg.length, "SHA-256");
        ECDSASigner ecdsaVerifier = new ECDSASigner();
        ecdsaVerifier.init(false, publicKey);
        return ecdsaVerifier.verifySignature(hashBytes, r, s);
    }

    @Override
    public byte[] decodeSignature(byte[] derSig) throws SignatureDecodeException {
        ECKey.ECDSASignature signature = ECKey.ECDSASignature.decodeFromDER(derSig);
        byte[] result = new byte[64];
        System.arraycopy(Utils.bigIntegerToBytes(signature.r, 32), 0, result, 0, 32);
        // Transaction Malleability
        BigInteger n = new BigInteger("115792089237316195423570985008687907852837564279074904382605163141518161494337");
        BigInteger halfN = new BigInteger("57896044618658097711785492504343953926418782139537452191302581570759080747168");
        BigInteger s = signature.s;
        if(s.compareTo(halfN) > 0 ){
            s = new BigInteger(1, n.subtract(s).toByteArray());
        }
        System.arraycopy(Utils.bigIntegerToBytes(s, 32), 0, result, 32, 32);
        return result;
    }
}
